/*
 * Copyright (c) 2009-2010 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.aionengine.gameserver.geoEngine.scene;

import com.aionengine.gameserver.geoEngine.bounding.BoundingBox;
import com.aionengine.gameserver.geoEngine.bounding.BoundingVolume;
import com.aionengine.gameserver.geoEngine.collision.Collidable;
import com.aionengine.gameserver.geoEngine.collision.CollisionResults;
import com.aionengine.gameserver.geoEngine.collision.bih.BIHTree;
import com.aionengine.gameserver.geoEngine.math.Matrix4f;
import com.aionengine.gameserver.geoEngine.math.Triangle;
import com.aionengine.gameserver.geoEngine.math.Vector2f;
import com.aionengine.gameserver.geoEngine.math.Vector3f;
import com.aionengine.gameserver.geoEngine.scene.VertexBuffer.Format;
import com.aionengine.gameserver.geoEngine.scene.VertexBuffer.Type;
import com.aionengine.gameserver.geoEngine.scene.VertexBuffer.Usage;
import com.aionengine.gameserver.geoEngine.scene.mesh.IndexBuffer;
import com.aionengine.gameserver.geoEngine.scene.mesh.IndexByteBuffer;
import com.aionengine.gameserver.geoEngine.scene.mesh.IndexIntBuffer;
import com.aionengine.gameserver.geoEngine.scene.mesh.IndexShortBuffer;
import com.aionengine.gameserver.geoEngine.utils.BufferUtils;
import com.aionengine.gameserver.geoEngine.utils.IntMap;
import com.aionengine.gameserver.geoEngine.utils.IntMap.Entry;

import java.nio.*;
import java.util.ArrayList;

public class Mesh {

    // TODO: Document this enum
    public enum Mode {
        Points,
        Lines,
        LineLoop,
        LineStrip,
        Triangles,
        TriangleStrip,
        TriangleFan,
        Hybrid
    }

    // private static final int BUFFERS_SIZE = VertexBuffer.Type.BoneIndex.ordinal() + 1;

    /**
     * The bounding volume that contains the mesh entirely. By default a BoundingBox (AABB).
     */
    private BoundingVolume meshBound = new BoundingBox();

    private CollisionData collisionTree = null;

    // private EnumMap<VertexBuffer.Type, VertexBuffer> buffers = new EnumMap<Type,
    // VertexBuffer>(VertexBuffer.Type.class);
    // private VertexBuffer[] buffers = new VertexBuffer[BUFFERS_SIZE];
    private IntMap<VertexBuffer> buffers = new IntMap<VertexBuffer>();
    private float pointSize = 1;
    private float lineWidth = 1;

    private transient int vertexArrayID = -1;

    private int vertCount = -1;
    private int elementCount = -1;
    private int maxNumWeights = -1; // only if using skeletal animation

    private int[] modeStart;

    private Mode mode = Mode.Triangles;

    private short collisionFlags = -1;

    public Mesh() {
    }

    public int[] getModeStart() {
        return modeStart;
    }

    public void setModeStart(int[] modeStart) {
        this.modeStart = modeStart;
    }

    public Mode getMode() {
        return mode;
    }

    public void setMode(Mode mode) {
        this.mode = mode;
        updateCounts();
    }

    public int getMaxNumWeights() {
        return maxNumWeights;
    }

    public void setMaxNumWeights(int maxNumWeights) {
        this.maxNumWeights = maxNumWeights;
    }

    public float getPointSize() {
        return pointSize;
    }

    public void setPointSize(float pointSize) {
        this.pointSize = pointSize;
    }

    public float getLineWidth() {
        return lineWidth;
    }

    public void setLineWidth(float lineWidth) {
        this.lineWidth = lineWidth;
    }

    /**
     * Locks the mesh so it cannot be modified anymore, thus optimizing its data.
     */
    public void setStatic() {
        for (Entry<VertexBuffer> entry : buffers) {
            entry.getValue().setUsage(Usage.Static);
        }
    }

    public void setStreamed() {
        for (Entry<VertexBuffer> entry : buffers) {
            entry.getValue().setUsage(Usage.Stream);
        }
    }

    public void setInterleaved() {
        ArrayList<VertexBuffer> vbs = new ArrayList<VertexBuffer>();
        for (Entry<VertexBuffer> entry : buffers) {
            vbs.add(entry.getValue());
        }
        // ArrayList<VertexBuffer> vbs = new ArrayList<VertexBuffer>(buffers.values());
        // index buffer not included when interleaving
        vbs.remove(getBuffer(Type.Index));

        int stride = 0; // aka bytes per vertex
        for (int i = 0; i < vbs.size(); i++) {
            VertexBuffer vb = vbs.get(i);
            // if (vb.getFormat() != Format.Float){
            // throw new UnsupportedOperationException("Cannot interleave vertex buffer.\n" +
            // "Contains not-float data.");
            // }
            stride += vb.componentsLength;
            vb.getData().clear(); // reset position & limit (used later)
        }

        VertexBuffer allData = new VertexBuffer(Type.InterleavedData);
        ByteBuffer dataBuf = BufferUtils.createByteBuffer(stride * getVertexCount());
        allData.setupData(Usage.Static, -1, Format.UnsignedByte, dataBuf);
        setBuffer(allData);

        for (int vert = 0; vert < getVertexCount(); vert++) {
            for (int i = 0; i < vbs.size(); i++) {
                VertexBuffer vb = vbs.get(i);
                switch (vb.getFormat()) {
                    case Float:
                        FloatBuffer fb = (FloatBuffer) vb.getData();
                        for (int comp = 0; comp < vb.components; comp++) {
                            dataBuf.putFloat(fb.get());
                        }
                        break;
                    case Byte:
                    case UnsignedByte:
                        ByteBuffer bb = (ByteBuffer) vb.getData();
                        for (int comp = 0; comp < vb.components; comp++) {
                            dataBuf.put(bb.get());
                        }
                        break;
                    case Half:
                    case Short:
                    case UnsignedShort:
                        ShortBuffer sb = (ShortBuffer) vb.getData();
                        for (int comp = 0; comp < vb.components; comp++) {
                            dataBuf.putShort(sb.get());
                        }
                        break;
                    case Int:
                    case UnsignedInt:
                        IntBuffer ib = (IntBuffer) vb.getData();
                        for (int comp = 0; comp < vb.components; comp++) {
                            dataBuf.putInt(ib.get());
                        }
                        break;
				default:
					break;
                }
            }
        }

        int offset = 0;
        for (VertexBuffer vb : vbs) {
            vb.setOffset(offset);
            vb.setStride(stride);

            // discard old buffer
            vb.setupData(vb.usage, vb.components, vb.format, null);
            offset += vb.componentsLength;
        }
    }

    private int computeNumElements(int bufSize) {
        switch (mode) {
            case Triangles:
                return bufSize / 3;
            case TriangleFan:
            case TriangleStrip:
                return bufSize - 2;
            case Points:
                return bufSize;
            case Lines:
                return bufSize / 2;
            case LineLoop:
                return bufSize;
            case LineStrip:
                return bufSize - 1;
            default:
                throw new UnsupportedOperationException();
        }
    }

    public void updateCounts() {
        if (getBuffer(Type.InterleavedData) != null)
            throw new IllegalStateException("Should update counts before interleave");

        VertexBuffer pb = getBuffer(Type.Position);
        VertexBuffer ib = getBuffer(Type.Index);
        if (pb != null) {
            vertCount = pb.getData().capacity() / pb.getNumComponents();
        }
        if (ib != null) {
            elementCount = computeNumElements(ib.getData().capacity());
        } else {
            elementCount = computeNumElements(vertCount);
        }
    }

    public int getTriangleCount(int lod) {
        return elementCount;
    }

    public int getTriangleCount() {
        return elementCount;
    }

    public int getVertexCount() {
        return vertCount;
    }

    public void setTriangleCount(int count) {
        this.elementCount = count;
    }

    public void setVertexCount(int count) {
        this.vertCount = count;
    }

    public void getTriangle(int index, Vector3f v1, Vector3f v2, Vector3f v3) {
        VertexBuffer pb = getBuffer(Type.Position);
        VertexBuffer ib = getBuffer(Type.Index);

        if (pb.getFormat() == Format.Float) {
            FloatBuffer fpb = (FloatBuffer) pb.getData();

            if (ib.getFormat() == Format.UnsignedShort) {
                // accepted format for buffers
                ShortBuffer sib = (ShortBuffer) ib.getData();

                // aquire triangle's vertex indices
                int vertIndex = index * 3;
                int vert1 = sib.get(vertIndex);
                int vert2 = sib.get(vertIndex + 1);
                int vert3 = sib.get(vertIndex + 2);

                BufferUtils.populateFromBuffer(v1, fpb, vert1);
                BufferUtils.populateFromBuffer(v2, fpb, vert2);
                BufferUtils.populateFromBuffer(v3, fpb, vert3);
            }
        }
    }

    public void getTriangle(int index, Triangle tri) {
        getTriangle(index, tri.get1(), tri.get2(), tri.get3());
        tri.setIndex(index);
    }

    public void getTriangle(int index, int[] indices) {
        VertexBuffer ib = getBuffer(Type.Index);
        if (ib.getFormat() == Format.UnsignedShort) {
            // accepted format for buffers
            ShortBuffer sib = (ShortBuffer) ib.getData();

            // aquire triangle's vertex indices
            int vertIndex = index * 3;
            indices[0] = sib.get(vertIndex);
            indices[1] = sib.get(vertIndex + 1);
            indices[2] = sib.get(vertIndex + 2);
        }
    }

    public int getId() {
        return vertexArrayID;
    }

    public void setId(int id) {
        if (vertexArrayID != -1)
            throw new IllegalStateException("ID has already been set.");

        vertexArrayID = id;
    }

    public void createCollisionData() {
        if (collisionTree != null) {
            return;
        }
        BIHTree tree = new BIHTree(this);
        tree.construct();
        collisionTree = tree;
    }

    public int collideWith(Collidable other, Matrix4f worldMatrix, BoundingVolume worldBound, CollisionResults results) {

        if (collisionTree == null) {
            createCollisionData();
        }

        return collisionTree.collideWith(other, worldMatrix, worldBound, results);
    }

    public void setBuffer(Type type, int components, FloatBuffer buf) {
        VertexBuffer vb = buffers.get(type.ordinal());
        if (vb == null) {
            if (buf == null)
                return;

            vb = new VertexBuffer(type);
            vb.setupData(Usage.Dynamic, components, Format.Float, buf);
            // buffers.put(type, vb);
            buffers.put(type.ordinal(), vb);
        } else {
            vb.setupData(Usage.Dynamic, components, Format.Float, buf);
        }
        updateCounts();
    }

    public void setBuffer(Type type, int components, float[] buf) {
        setBuffer(type, components, BufferUtils.createFloatBuffer(buf));
    }

    public void setBuffer(Type type, int components, IntBuffer buf) {
        VertexBuffer vb = buffers.get(type.ordinal());
        if (vb == null) {
            vb = new VertexBuffer(type);
            vb.setupData(Usage.Dynamic, components, Format.UnsignedInt, buf);
            buffers.put(type.ordinal(), vb);
            updateCounts();
        }
    }

    public void setBuffer(Type type, int components, int[] buf) {
        setBuffer(type, components, BufferUtils.createIntBuffer(buf));
    }

    public void setBuffer(Type type, int components, ShortBuffer buf) {
        VertexBuffer vb = buffers.get(type.ordinal());
        if (vb == null) {
            vb = new VertexBuffer(type);
            vb.setupData(Usage.Dynamic, components, Format.UnsignedShort, buf);
            buffers.put(type.ordinal(), vb);
            updateCounts();
        }
    }

    public void setBuffer(Type type, int components, byte[] buf) {
        setBuffer(type, components, BufferUtils.createByteBuffer(buf));
    }

    public void setBuffer(Type type, int components, ByteBuffer buf) {
        VertexBuffer vb = buffers.get(type.ordinal());
        if (vb == null) {
            vb = new VertexBuffer(type);
            vb.setupData(Usage.Dynamic, components, Format.UnsignedByte, buf);
            buffers.put(type.ordinal(), vb);
            updateCounts();
        }
    }

    public void setBuffer(VertexBuffer vb) {
        if (buffers.containsKey(vb.getBufferType().ordinal()))
            throw new IllegalArgumentException("Buffer type already set: " + vb.getBufferType());

        buffers.put(vb.getBufferType().ordinal(), vb);
    }

    public void clearBuffer(VertexBuffer.Type type) {
        buffers.remove(type.ordinal());
    }

    public void setBuffer(Type type, int components, short[] buf) {
        setBuffer(type, components, BufferUtils.createShortBuffer(buf));
    }

    public VertexBuffer getBuffer(Type type) {
        return buffers.get(type.ordinal());
    }

    public FloatBuffer getFloatBuffer(Type type) {
        VertexBuffer vb = getBuffer(type);
        if (vb == null)
            return null;

        return (FloatBuffer) vb.getData();
    }

    public ShortBuffer getShortBuffer(Type type) {
        VertexBuffer vb = getBuffer(type);
        if (vb == null)
            return null;

        return (ShortBuffer) vb.getData();
    }

    public IndexBuffer getIndexBuffer() {
        VertexBuffer vb = getBuffer(Type.Index);
        if (vb == null)
            return null;

        Buffer buf = vb.getData();
        if (buf instanceof ByteBuffer) {
            return new IndexByteBuffer((ByteBuffer) buf);
        } else if (buf instanceof ShortBuffer) {
            return new IndexShortBuffer((ShortBuffer) buf);
        } else if (buf instanceof IntBuffer) {
            return new IndexIntBuffer((IntBuffer) buf);
        } else {
            throw new UnsupportedOperationException("Index buffer type unsupported: " + buf.getClass());
        }
    }

    public void scaleTextureCoordinates(Vector2f scaleFactor) {
        VertexBuffer tc = getBuffer(Type.TexCoord);
        if (tc == null)
            throw new IllegalStateException("The mesh has no texture coordinates");

        if (tc.getFormat() != VertexBuffer.Format.Float)
            throw new UnsupportedOperationException("Only float texture coord format is supported");

        if (tc.getNumComponents() != 2)
            throw new UnsupportedOperationException("Only 2D texture coords are supported");

        FloatBuffer fb = (FloatBuffer) tc.getData();
        fb.clear();
        for (int i = 0; i < fb.capacity() / 2; i++) {
            float x = fb.get();
            float y = fb.get();
            fb.position(fb.position() - 2);
            x *= scaleFactor.getX();
            y *= scaleFactor.getY();
            fb.put(x).put(y);
        }
        fb.clear();
    }

    public void updateBound() {
        VertexBuffer posBuf = getBuffer(VertexBuffer.Type.Position);
        if (meshBound == null)
            meshBound = new BoundingBox();
        if (posBuf != null) {
            meshBound.computeFromPoints((FloatBuffer) posBuf.getData());
        }
    }

    public BoundingVolume getBound() {
        return meshBound;
    }

    public void setBound(BoundingVolume modelBound) {
        meshBound = modelBound;
    }

    public IntMap<VertexBuffer> getBuffers() {
        return buffers;
    }

    public short getCollisionFlags() {
        return collisionFlags;
    }

    public void setCollisionFlags(short collisionFlags) {
        this.collisionFlags = collisionFlags;
    }

    public byte getMaterialId() {
        return (byte) (collisionFlags & 0xFF);
    }

    public byte getIntentions() {
        return (byte) (collisionFlags >> 8);
    }

}
